﻿using System;
using System.Globalization;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace EventbriteService
{
    /// <summary>
    /// Based on Json parser in JavaScript found in JavaScript: The Good Parts.
    /// </summary>
    public sealed class JsonReader
    {
        int _pos;
        char _currentChar;
        StringBuilder _text;
        Dictionary<char, object> escapee = new Dictionary<char, object>() { 
                                                         { '"', '"' }, { '\\', '\\' }, 
                                                         { '/', '/' }, { 'n', '\n' },
                                                         { 'b', 'b' }, { 'f', '\f' },  
                                                         { 'r', '\r' }, { 't', '\t' } };
        Func<char, int, ArgumentException> _error = (t, p) => new ArgumentException(string.Format("Expected {0} in position: {1}", t, p), "SyntaxError");
        public JsonReader(string json)
        {
            _text = new StringBuilder(json);
            _pos = 0;
            _currentChar = Characters.EmptySpace;

        }

        public object Parse()
        {
           return Value();
        }

        private object ParseObject()
        {

            if (_currentChar == Characters.CurlyBracketOpen)
            {
                Next(Characters.CurlyBracketOpen);
                Skip();
                if (_currentChar == Characters.CurlyBracketClose)
                {
                    Next(Characters.CurlyBracketClose);
                    return null;
                }

                Dictionary<string, object> obj = new Dictionary<string, object>();
                while (_currentChar != Characters.EmptySpace)
                {
                    string key = ParseString();
                    Skip();
                    Next(Characters.Colon);
                    obj[key] = Value();
                    Skip();
                    if (_currentChar == Characters.CurlyBracketClose)
                    {
                        Next(Characters.CurlyBracketClose);
                        return obj;
                    }
                    Next(Characters.Comma);
                    Skip();
                }

            }
            return null;
        }

        private object Value()
        {
            Skip();
            switch (_currentChar)
            {
                case Characters.CurlyBracketOpen:
                    Dictionary<string, object> obj = new Dictionary<string, object>();
                    obj.Add("jobject", ParseObject());
                    return obj;
                case Characters.SquareBracketOpen:
                    return ParseArray();
                case Characters.Quotation:
                    return ParseString();
                case Characters.HyphenMinus:
                    return ParseNumber();
                default:
                    return Char.IsNumber(_currentChar) ? ParseNumber() : Word();
            }
        }

        private object ParseArray()
        {
            System.Collections.ArrayList al = new System.Collections.ArrayList();
            if (_currentChar == Characters.SquareBracketOpen)
            {
                Next(Characters.SquareBracketOpen);
                Skip();
                if (_currentChar == Characters.SquareBracketClose)
                {
                    Next(Characters.SquareBracketClose);
                    return al;
                }

                while (_currentChar != Characters.EmptySpace)
                {
                    al.Add(Value());
                    Skip();

                    if (_currentChar == Characters.SquareBracketClose)
                    {
                        Next(Characters.SquareBracketClose);
                        return al;
                    }
                    Next(Characters.Comma);
                    Skip();
                }

            }
            return al;

        }

        private object ParseNumber()
        {
            StringBuilder s = new StringBuilder();
            bool isInt = true;
            bool isNegative = false;

            if (_currentChar == Characters.HyphenMinus)
            {
                // negative number
                s.Append(Characters.HyphenMinus);
                Next(Characters.HyphenMinus);
                isNegative = true;
                // special case: infinity
                if (_currentChar == 'I')
                    return ParseInfinity(Characters.HyphenMinus);
            }

            // parse hex and octal as string
            if (IsHexOrOctal())
                return ParseNumAsString(isNegative);

            // build number
            while (Char.IsNumber(_currentChar))
            {
                s.Append(_currentChar);
                Next(null);
            }

            if (_currentChar == Characters.Dot)
            {
                // decimal point
                s.Append(_currentChar);

                // characters right of decimal point
                while (Next(null) != Char.MinValue && Char.IsNumber(_currentChar))
                {
                    s.Append(_currentChar);

                }
                isInt = false;
            }

            if (IsExponentialNotation())
            {
                s.Append(_currentChar);
                Next(null);
                if (IsPlusOrMinus())
                {
                    s.Append(_currentChar);
                    Next(null);
                }

                while (Char.IsNumber(_currentChar))
                {
                    s.Append(_currentChar);
                    Next(null);
                }
                isInt = false;

            }
            if (isInt)
                return long.Parse(s.ToString());

            return decimal.Parse(s.ToString(), NumberStyles.Any);

        }


        // parsing hex and octal
        private string ParseNumAsString(bool isNegative)
        {
            StringBuilder sb = new StringBuilder();

            if (isNegative)
                sb.Append(Characters.HyphenMinus);

            sb.Append(_currentChar);
            Next(null);

            if (_currentChar == 'x')
            {
                sb.Append(_currentChar);
                Next('x');
            }

            while (Char.IsNumber(_currentChar) || IsHex())
            {
                sb.Append(_currentChar);
                Next(null);
            }
            return sb.ToString();
        }

        private string ParseString()
        {
            StringBuilder s = new StringBuilder(255);

            if (_currentChar == Characters.Quotation)
            {
                while (Next(null) != char.MinValue)
                {
                    if (_currentChar == Characters.Quotation)
                    {
                        // end of string
                        Next(null);
                        return s.ToString();
                    }
                    else if (_currentChar == Characters.Backslash)
                    {
                        Next(null);
                        if (_currentChar == Characters.Unicode)
                        {
                            // build unicode value
                            char[] hex = new char[4];
                            for (int i = 0; i < 4; i++)
                            {
                                hex[i] = Next(null);
                            }
                            s.Append(Convert.ToChar(int.Parse(new String(hex, 0, 4), System.Globalization.NumberStyles.HexNumber,
                                                                System.Globalization.NumberFormatInfo.InvariantInfo)));
                        }
                        else if (escapee.ContainsKey(_currentChar))
                        {
                            // add special characters
                            s.Append(escapee[_currentChar]);
                        }
                        else
                        {
                            break;
                        }
                    }
                    else
                    {
                        // all cases covered, add current character
                        s.Append(_currentChar);
                    }
                }
            }
            return s.ToString();
        }

        private object Word()
        {
            switch (_currentChar)
            {
                case 't':
                    Next('t');
                    Next('r');
                    Next('u');
                    Next('e');
                    return true;
                case 'f':
                    Next('f');
                    Next('a');
                    Next('l');
                    Next('s');
                    Next('e');
                    return false;
                case 'n':
                    Next('n');
                    Next('u');
                    Next('l');
                    Next('l');
                    return null;
                case 'I':
                    return ParseInfinity('+');
                case 'N':
                    Next('N');
                    Next('a');
                    Next('N');
                    return double.NaN;
                default:
                    throw _error(_currentChar, _pos);
            }
        }

        private object ParseInfinity(char prefix)
        {
            if (_currentChar == 'I')
            {
                Next('I');
                Next('n');
                Next('f');
                Next('i');
                Next('n');
                Next('i');
                Next('t');
                Next('y');
            }
            if (prefix == Characters.HyphenMinus)
                return Double.NegativeInfinity;
            return Double.PositiveInfinity;
        }

        #region Navigation members
        private void Skip()
        {
            while (_currentChar <= Characters.EmptySpace && _currentChar != char.MinValue)
            {
                Next(null);
            }
        }

        private char Peek(int pos)
        {
            if (pos < _text.Length)
                return _text[pos];
            return char.MinValue;
        }

        private char Next(char? c)
        {
            if (c.HasValue && c != _currentChar)
                throw _error(c.Value, _pos);

            if (_pos < _text.Length)
            {
                _currentChar = _text[_pos];
                _pos++;
            }
            else
            {
                _currentChar = char.MinValue;
            }
            return _currentChar;

        }
        #endregion

        #region Conditions
        private bool IsHexOrOctal()
        {
            return _currentChar == Characters.Zero &&
                                    Peek(_pos) != Characters.Dot &&
                                    Char.ToLowerInvariant(Peek(_pos)) != 'e';
        }

        private bool IsExponentialNotation()
        {
            return _currentChar == 'e' || _currentChar == 'E';
        }

        private bool IsPlusOrMinus()
        {
            return _currentChar == Characters.HyphenMinus || _currentChar == Characters.Plus;
        }

        private bool IsHex()
        {
            return Char.ToLowerInvariant(_currentChar) >= 'a' && char.ToLowerInvariant(_currentChar) <= 'f';
        }
        #endregion

        private class Characters
        {
            public const char Comma = ',';
            public const char Dot = '.';
            public const char SquareBracketClose = ']';
            public const char SquareBracketOpen = '[';
            public const char CurlyBracketOpen = '{';
            public const char CurlyBracketClose = '}';
            public const char EmptySpace = ' ';
            public const char Zero = '0';
            public const char Nine = '9';
            public const char HyphenMinus = '-';
            public const char Plus = '+';
            public const char Quotation = '"';
            public const char Backslash = '\\';
            public const char Unicode = 'u';
            public const char Colon = ':';
        }
    }
}
